package top.yangguangmc.sunshine_anticheat.check.checks.combat;

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.jetbrains.annotations.NotNull;
import top.yangguangmc.sunshine_anticheat.check.Check;
import top.yangguangmc.sunshine_anticheat.check.CheckCategory;
import top.yangguangmc.sunshine_anticheat.check.CheckSetting;
import top.yangguangmc.sunshine_anticheat.check.checks.player.AutoToolCheck;
import top.yangguangmc.sunshine_anticheat.utils.Utils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.LinkedBlockingDeque;

import static top.yangguangmc.sunshine_anticheat.utils.Builtin.*;

public class AutoClickerCheck extends Check {
    private final Map<UUID, Set<CpsChecker>> checkers = new ConcurrentHashMap<>();

    @CheckSetting(configName = "max-possible-cps")
    int maxCps = 20;
    @CheckSetting(configName = "min-accurate-cps")
    int minAccurateCps = 7;
    @CheckSetting(configName = "min-deviation")
    double minDeviation = 10;

    public AutoClickerCheck() {
        super("AutoClicker", "auto-clicker", CheckCategory.COMBAT);
        Thread thread = new Thread(() -> {
            while (ssac.isEnabled()) {
                checkers.forEach((uuid, set) -> set.forEach(CpsChecker::onTick));
            }
        }, "CPS Calculator");
        thread.setDaemon(true);
        thread.start();
        Utils.scheduleLoop(() -> checkers.forEach((uuid, set) -> set.forEach(cpsChecker -> cpsChecker.updateSetting(minAccurateCps, minDeviation, maxCps))), 20);
    }

//    @EventHandler
//    public void onAttack(EntityDamageByEntityEvent event) {
//        if (!validateCheckable(event.getDamager())) return;
//
//    }

    @EventHandler
    public void onInteract(PlayerInteractEvent event) {
        Player player = event.getPlayer();
        if (!validateCheckable(player)) return;
        checkers.putIfAbsent(player.getUniqueId(), new ConcurrentSkipListSet<>());
        Set<CpsChecker> set = checkers.get(player.getUniqueId());
        for (CpsChecker.Type type : CpsChecker.Type.values()) {
            if (set.stream().noneMatch(cpsChecker -> cpsChecker.type == type)) {
                CpsChecker e = new CpsChecker(type);
                e.updateSetting(minAccurateCps, minDeviation, maxCps);
                set.add(e);
            }
        }
        if ((event.getAction() == Action.LEFT_CLICK_BLOCK || event.getAction() == Action.LEFT_CLICK_AIR)
                && !ssac.getCheckManager().getCheckByClass(AutoToolCheck.class).isPlayerDigging(player)) {
            for (CpsChecker checker : set) {
                if (checker.type == CpsChecker.Type.LEFT) {
                    CpsChecker.CheckResult result = checker.onClick();
                    if (result.failed) {
                        failCheck(player, result.vl, result.verbose + " (" + checker + ")");
                    }
                }
            }
        } else if (event.getAction() == Action.RIGHT_CLICK_BLOCK || event.getAction() == Action.RIGHT_CLICK_AIR) {
            for (CpsChecker checker : set) {
                if (checker.type == CpsChecker.Type.RIGHT) {
                    CpsChecker.CheckResult result = checker.onClick();
                    if (result.failed) {
                        failCheck(player, result.vl, result.verbose + " (" + checker + ")");
                    }
                }
            }
        }
    }

    @EventHandler
    public void onSneak(PlayerToggleSneakEvent event) {
        Player player = event.getPlayer();
        if (!validateCheckable(player)) return;
        if (event.isSneaking()) {
            checkers.putIfAbsent(player.getUniqueId(), new ConcurrentSkipListSet<>());
            Set<CpsChecker> set = checkers.get(player.getUniqueId());
            for (CpsChecker.Type type : CpsChecker.Type.values()) {
                if (set.stream().noneMatch(cpsChecker -> cpsChecker.type == type)) {
                    CpsChecker e = new CpsChecker(type);
                    e.updateSetting(minAccurateCps, minDeviation, maxCps);
                    set.add(e);
                }
            }
            for (CpsChecker checker : set) {
                if (checker.type == CpsChecker.Type.KEY_SHIFT) {
                    CpsChecker.CheckResult result = checker.onClick();
                    if (result.failed) {
                        failCheck(player, result.vl, result.verbose + " (" + checker + ")");
                    }
                }
            }
        }
    }

    static class CpsChecker implements Comparable<CpsChecker> {
        final Type type;
        final Deque<Long> timeStamps = new LinkedBlockingDeque<>();
        int lastCps = 0;

        int maxCps;
        int minAccurateCps;
        double minDeviation;

        CpsChecker(Type type) {
            this.type = type;
        }

        private void updateSetting(int minAccurateCps, double minDeviation, int maxCps) {
            this.minAccurateCps = minAccurateCps;
            this.minDeviation = minDeviation;
            this.maxCps = maxCps;
        }

        private void onTick() {
            while (!timeStamps.isEmpty() && System.currentTimeMillis() - timeStamps.getFirst() > 1000) {
                timeStamps.pollFirst();
            }
        }

        public CheckResult onClick() {
            // Duplicate click in one tick
            long last = 0;
            try {
                last = timeStamps.getLast();
            } catch (NoSuchElementException ignored) {
            }
            timeStamps.addLast(System.currentTimeMillis()); // This line is not a part of the single check, but the whole method!
            if (System.currentTimeMillis() - last < 25) {   //TODO: 误报较多
                return new CheckResult(true, 3, getCps(), "Two clicks in one single tick.");
            }

            // Too high CPS check
            if (getCps() > maxCps && getCps() >= lastCps) {
                return new CheckResult(true, 5, getCps(), "Clicked impossibly fast (>" + maxCps + " CPS).");
            }

            // Accurate check
            if (getCps() >= minAccurateCps) {
                long[] deltas = new long[getCps() - 1];
                int i = 0;
                long lastTimeStamp = System.currentTimeMillis();
                for (Long timeStamp : timeStamps) {
                    if (i > 0) {
                        deltas[i - 1] = (timeStamp - lastTimeStamp);
                    }
                    lastTimeStamp = timeStamp;
                    i++;
                }
                int n = deltas.length;
                if (n > 1) {
                    long sum = 0;
                    for (long delta : deltas) sum += delta;
                    long avg = sum / n;
                    long sumSq = 0;
                    for (long delta : deltas) sumSq += sqr(delta);
                    double stdDeviation = sqrt((double) sumSq / n - sqr(avg));
                    if (stdDeviation < minDeviation) {
                        return new CheckResult(true, stdDeviation == 0 ? 3 : 1, getCps(),
                                String.format("Click rate too accurate (StandardDeviation: %.2f)", stdDeviation));
                    }
                }
            }

            // Too many clicks in a short period. TODO: 存在DC误报
            Deque<Long> copy = new LinkedList<>(timeStamps);
            while (!copy.isEmpty() && System.currentTimeMillis() - copy.getFirst() > 200) {
                copy.pollFirst();
            }
            if (copy.size() > 4) {
                return new CheckResult(true, 2, getCps(), "Too many clicks in a short period (" + copy.size() + " clicks in " + 200 + "ms).");
            }

            lastCps = getCps();
            return new CheckResult(false, 0, getCps(), null);
        }

        public int getCps() {
            return timeStamps.size();
        }

        @Override
        public int compareTo(@NotNull AutoClickerCheck.CpsChecker cpsChecker) {
            return Integer.compare(cpsChecker.timeStamps.size(), timeStamps.size());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CpsChecker that = (CpsChecker) o;
            return type == that.type;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(type);
        }

        @Override
        public String toString() {
            return "CpsChecker{" +
                    "type=" + type +
                    ", cps=" + getCps() +
                    '}';
        }

        enum Type {
            LEFT,
            RIGHT,
            KEY_SHIFT,
        }

        static class CheckResult {
            final boolean failed;
            final int vl;
            final int cps;
            final String verbose;

            CheckResult(boolean failed, int vl, int cps, String verbose) {
                this.failed = failed;
                this.vl = vl;
                this.cps = cps;
                this.verbose = verbose;
            }

            @Override
            public String toString() {
                return "{" +
                        "vl=" + vl +
                        ", cps=" + cps +
                        ", verbose='" + verbose + '\'' +
                        '}';
            }
        }
    }
}
